Flexible Return To Office Options
Authored by: Angie Hollingworth
Duration: 90 mins
Level: Intermediate
Pre-requisite Skills: Ptyhon, Jupyter
Scenario

The City Of Melbourne Council is looking for ideas to bring people back into the city post covid, especially as a lot of businesses left the city permanently and the foot traffic from city workers has also seriously decreased.

An option to get people back into Melbourne City is to utilise the coworking businesses that are all throughout the city, and encourage workers in the city to support local businesses (in this analysis food and drink businesses) that are nearby to those office spaces.

As people have been working from home, and no longer might have an office to go to and collaborate, this use case provides a way for colleagues and friends to find a coworking office space that is central to everyone, and explore the café, restaurant, bistro businesses that may be nearby. This type of exploration into the city of Melbourne can encourage occasional city workers to move around the city, trying different coworking office spaces and supporting the local businesses around them.

What this use case will teach you

At the end of this use case you will:

  • Understand the use case
  • Be able to map data in layers (clusters) on folium maps
  • Calculate the midpoint betwene a series of locations (in python)
  • Create interactive widgets for controlling variable values
Python Library Installs

The below libraries are required to run this notebook. If you do not have any of then installed, or you get an error, be sure to pip install them like below:

!pip install folium

In [2]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import requests
import math
import random
import ipywidgets as widgets
import json
import folium

from math import radians, cos, sin, asin, sqrt
from folium.plugins import MarkerCluster
Datasets

Below are the dataset that have been chosen form the City of Melbourne Open Data website

cafes-and-restaurants-with-seating-capacity

coworking-spaces

metro-train-stations-with-accessibility-information

In [3]:
seats_dataset= 'cafes-and-restaurants-with-seating-capacity'
coworking_dataset = 'coworking-spaces'
trains = 'metro-train-stations-with-accessibility-information'

First we will start by setting up a bse URL for the 3 datasets to use as they have some general informatioon that is shared.

In [4]:
BASE_URL = 'https://data.melbourne.vic.gov.au/api/v2/catalog/datasets/'
BASE_URL_v1 = 'https://data.melbourne.vic.gov.au/api/records/1.0/search/?dataset='
NUMBER_OF_RECORDS = 300

Co-Working Dataset

The coworking spaces listed are currently only located within the City of Melbourne municipal boundary. The list is not exhaustive. There may be other coworking spaces within the City of Melbourne municipality.

In [5]:
# check the number records for Co-Working paces
num_records_url = f'{BASE_URL_v1}{coworking_dataset}'

r = requests.get(num_records_url)
data_size = r.json()['nhits']  
print(f'({coworking_dataset}) Total Records: {data_size}')
(coworking-spaces) Total Records: 66

As the dataset will fit in one API call (<300), let's get the data as a JSON dump

In [6]:
# Build up the whole URL using f strings
url_filter = f'exports/json?limit={NUMBER_OF_RECORDS}&offset=0&timezone=UTC'
url = f'{BASE_URL}{coworking_dataset}/{url_filter}'

# Call the API to get the dataset
coworking_result = requests.get(url)
coworking_result_json = coworking_result.json()
coworking = pd.DataFrame(coworking_result_json)
In [7]:
# View the dataset
coworking.head()
Out[7]:
organisation address website latitude longitude geopoint
0 11th Space Level 11/580 Collins St, Melbourne VIC 3000 https://11thspace.com/ -37.818523 144.955364 {'lon': 144.9553637, 'lat': -37.818523}
1 360 Collins 360 Collins Street, Melbourne 3000 https://officespace.com.au/melbourne/153369/ -37.816002 144.962311 {'lon': 144.962311, 'lat': -37.816002}
2 ACMI X Level 4, 2 Kavanagh Street, Southbank 3006 https://www.acmi.net.au/acmi-x/ -37.821801 144.967594 {'lon': 144.9675938, 'lat': -37.8218014}
3 Clik Collective 2 Chelmsford Street, Kensington 3031 http://clikcollective.com.au -37.796415 144.931696 {'lon': 144.9316956, 'lat': -37.7964148}
4 Hatch Quarter 7/677 La Trobe Street, Docklands 3008 http://www.hatchquarter.com.au/ -37.814600 144.947310 {'lon': 144.94731, 'lat': -37.8146}

Cafe, Restuarant, Bistro Seats

Data collected as part of the City of Melbourne's Census of Land Use and Employment (CLUE). The data covers the period 2002-2021. It show business establishments with their trading name, business address, industry (ANZSIC4) classification, number of dining seats (classified by indoor/outdoor), location and CLUE block and small area designation.

In [8]:
#Check the size of the Seats dataset

seats_filter = '&refine.census_year=2021'
num_records_url = f'{BASE_URL_v1}{seats_dataset}&q=&rows=1&start=0{seats_filter}'

r = requests.get(num_records_url)
data_size = r.json()['nhits']  
print(f'({seats_dataset}) Total Records: {data_size}')
(cafes-and-restaurants-with-seating-capacity) Total Records: 3157

Increase the rows requested to 4,000 to capture the whole dataset for 2021

In [9]:
# Build the URL
num_records_url = f'{BASE_URL_v1}{seats_dataset}&q=&rows=4000&start=0{seats_filter}'

# Call the API and get the dataset
r = requests.get(num_records_url)
seats_json = r.json()
In [10]:
# Limit the retuend JSON data to just the 'records'
records = seats_json['records']

# Flatten the JSON data to a table
records_df = pd.json_normalize(records)

#Get the new columns names for the JSON data
columns = records_df.columns.to_list()
columns
Out[10]:
['datasetid',
 'recordid',
 'record_timestamp',
 'fields.location',
 'fields.seating_type',
 'fields.census_year',
 'fields.property_id',
 'fields.base_property_id',
 'fields.trading_name',
 'fields.block_id',
 'fields.industry_anzsic4_description',
 'fields.number_of_seats',
 'fields.building_address',
 'fields.clue_small_area',
 'fields.business_address',
 'fields.industry_anzsic4_code',
 'fields.longitude',
 'fields.latitude',
 'geometry.type',
 'geometry.coordinates']

Limit the dataset to only the oclumns which have "field" in the name indicating that they are the field values and not the meta data.

In [11]:
seats = pd.DataFrame(records_df[[c for c in columns if c.startswith('fields') ==True]])
column_names = [c.replace('fields.','') for c in seats.columns.to_list()]
seats.columns= column_names

# Remove unwanted columns form the dataset
seats.drop(columns=['location', 'census_year', 'property_id', 'base_property_id', 'block_id', 'clue_small_area', 'industry_anzsic4_code'], inplace=True)
seats.rename(columns = {'industry_anzsic4_description':'business_type'}, inplace=True)
seats.head(2)
Out[11]:
seating_type trading_name business_type number_of_seats building_address business_address longitude latitude
0 Seats - Indoor Tokyo Maki Cafes and Restaurants 44 545-557 Flinders Street MELBOURNE VIC 3000 547 Flinders Street MELBOURNE VIC 3000 144.95651 -37.82098
1 Seats - Indoor Domino's Pizza Takeaway Food Services 12 545-557 Flinders Street MELBOURNE VIC 3000 553 Flinders Street MELBOURNE VIC 3000 144.95651 -37.82098
In [12]:
# remove duplicate rows for row with max seats for business
idx = seats.groupby(["trading_name", "business_address"])["number_of_seats"].idxmax()

# Subset the DataFrame using the index found above
seats = seats.loc[idx].reset_index(drop=True)

Have a look at the types of buinesses in the dataset

In [13]:
seats['business_type'].unique()
Out[13]:
array(['Cafes and Restaurants', 'Takeaway Food Services',
       'Clubs (Hospitality)', 'Non-Residential Property Operators',
       'Catering Services', 'Accommodation', 'Pubs, Taverns and Bars',
       'Car Retailing',
       'Sports and Physical Recreation Venues, Grounds and Facilities Operation',
       'Other Specialised Food Retailing',
       'Bakery Product Manufacturing (Non-factory based)',
       'Fruit and Vegetable Retailing', 'Flower Retailing',
       'Motion Picture Exhibition', 'Performing Arts Venue Operation',
       'Casino Operation', 'Newspaper and Book Retailing',
       'Supermarket and Grocery Stores',
       'Amusement and Other Recreational Activities n.e.c.',
       'Horse and Dog Racing Administration and Track Operation',
       'Other Gambling Activities',
       'Automotive Body, Paint and Interior Repair',
       'Zoological and Botanical Gardens Operation',
       'Other Administrative Services n.e.c.',
       'Other Store-Based Retailing n.e.c.', 'Performing Arts Operation',
       'Telecommunication Goods Wholesaling',
       'Other Food Product Manufacturing n.e.c.', 'Museum Operation',
       'Other Interest Group Services n.e.c.', 'Religious Services',
       'Health and Fitness Centres and Gymnasia Operation'], dtype=object)

Limit the dataset to just food and drink services.

In [14]:
business_types = ['Cafes and Restaurants', 'Takeaway Food Services','Pubs, Taverns and Bars']

seats = pd.DataFrame(seats[seats['business_type'].isin(business_types)]).reset_index(drop=True)
seats.shape
Out[14]:
(2042, 8)
In [15]:
# Selected seating types
seating_types = {'food':['Cafes and Restaurants', 'Takeaway Food Services'],
                'drink': ['Pubs, Taverns and Bars']}
In [16]:
# Look at the types of seats
seats['seating_type'].unique()
Out[16]:
array(['Seats - Indoor', 'Seats - Outdoor'], dtype=object)
In [17]:
# get an overview of the Trading Names
seats['trading_name'].unique()
Out[17]:
array(['+39 Pizzeria & Antipasteria', '1000 Wat', '11 Inch Pizza', ...,
       'Zouki on the Park', 'Zuppa', 'san churro docklands'], dtype=object)

Build the 4 types of "seats" datasets based on food, drinks, inside & outside

In [18]:
food = pd.DataFrame(seats[seats['business_type'].isin(seating_types['food'])]).reset_index(drop=True)
food.shape
Out[18]:
(1918, 8)
In [19]:
drink = pd.DataFrame(seats[seats['business_type'].isin(seating_types['drink'])]).reset_index(drop=True)
drink.shape
Out[19]:
(124, 8)
In [20]:
indoor = pd.DataFrame(seats[seats['seating_type']=='Seats - Indoor']).reset_index(drop=True)
indoor.shape
Out[20]:
(1794, 8)
In [21]:
outdoor = pd.DataFrame(seats[seats['seating_type']=='Seats - Outdoor']).reset_index(drop=True)
outdoor.shape
Out[21]:
(248, 8)

Train Station Dataset

This data contains locations of train stations and their accessibility information, such as hearing aid information.

In [22]:
# Get Train stations
url = f'{BASE_URL}{trains}/{url_filter}'
trains_result = requests.get(url)
trains_result_json = trains_result.json()
train_stations = pd.DataFrame(trains_result_json)

# View the first two rows of the dataset
train_stations.head(2)
Out[22]:
geo_point_2d geo_shape he_loop lift pids station
0 {'lon': 145.07955800000002, 'lat': -37.8688429... {'type': 'Feature', 'geometry': {'coordinates'... No No No Alamein
1 {'lon': 144.82470999999998, 'lat': -37.7776559... {'type': 'Feature', 'geometry': {'coordinates'... No No Dot Matrix Albion
In [23]:
# Create 2 columns for lat and lon values
train_stations['lat'] = train_stations['geo_point_2d'].apply(lambda x: x['lat'])
train_stations['lon'] = train_stations['geo_point_2d'].apply(lambda x: x['lon'])
train_stations.head(2)
Out[23]:
geo_point_2d geo_shape he_loop lift pids station lat lon
0 {'lon': 145.07955800000002, 'lat': -37.8688429... {'type': 'Feature', 'geometry': {'coordinates'... No No No Alamein -37.868843 145.079558
1 {'lon': 144.82470999999998, 'lat': -37.7776559... {'type': 'Feature', 'geometry': {'coordinates'... No No Dot Matrix Albion -37.777656 144.824710

Reduce the trains Dataset to a selected distance form the CBD

In [24]:
# Create a function for controllling teh slider
def save_value(change):
    global selected_value
    selected_value = change['new']
In [25]:
# Create an IntSlider widget with range from 200 to 5000
slider = widgets.IntSlider(value = 5, min=1, max=10, step=1)

# Attach the save_value function to the slider's value attribute
slider.observe(save_value, names='value')
In [26]:
print('Select the radius in KMs from the CBD to limit Train stations:')
display(slider)
Select the radius in KMs from the CBD to limit Train stations:
In [27]:
# The below function will find the suburbs that fall within the sleected raduis from the CBD

def haversine(lat1, lon1, lat2, lon2):
    R = 6371.0 # radius of the Earth in kilometers
    lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
    dlat = lat2 - lat1
    dlon = lon2 - lon1
    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
    c = 2 * asin(sqrt(a))
    distance = R * c
    return distance

Narrow the tran station dataset by selecting how far out of the CBD are train stations being included.

In [28]:
# Melbourne CBS lat / lon location
lat = -37.81368709240999 # latitude of the location
lon = 144.95738102347036 # longitude of the location

# set the slider selection to a variable
radius = slider.value
In [29]:
# Filter the DataFrame to select only rows that are within the radius
train_stations_CBD = train_stations[train_stations.apply(lambda row: haversine(row['lat'], row['lon'], lat, lon) <= radius, axis=1)].reset_index(drop=True)

# Print the filtered DataFrame
train_stations_CBD.head(3)
Out[29]:
geo_point_2d geo_shape he_loop lift pids station lat lon
0 {'lon': 145.0076200000001, 'lat': -37.827631} {'type': 'Feature', 'geometry': {'coordinates'... No No Dot Matrix Burnley -37.827631 145.007620
1 {'lon': 144.963757, 'lat': -37.809699999999964} {'type': 'Feature', 'geometry': {'coordinates'... Yes Yes LCD Melbourne Central -37.809700 144.963757
2 {'lon': 144.97340800000006, 'lat': -37.8119289... {'type': 'Feature', 'geometry': {'coordinates'... Yes Yes LCD Parliament -37.811929 144.973408

Get a list of Train station names (We will use this to create our button selectors)

In [30]:
station_list = train_stations_CBD['station'].to_list()
station_list
Out[30]:
['Burnley',
 'Melbourne Central',
 'Parliament',
 'West Richmond',
 'Clifton Hill',
 'Collingwood',
 'Flagstaff',
 'Flemington Bridge',
 'Jewell',
 'Jolimont',
 'Prahran',
 'Rushall',
 'East Richmond',
 'Flinders Street',
 'Kensington',
 'Macaulay',
 'Newmarket',
 'North Melbourne',
 'North Richmond',
 'Richmond',
 'Royal Park',
 'South Kensington',
 'South Yarra',
 'Southern Cross',
 'Victoria Park']
Optional Cells

The next three cells can be omitted if you are going to be selecting the friend group size and home stations yourself.

In [31]:
# Create an IntSlider widget with range from 200 to 5000
friend_slider = widgets.IntSlider(value = 7, min=1, max=12, step=1)

# Attach the save_value function to the slider's value attribute
friend_slider.observe(save_value, names='value')
In [32]:
print('Select the number of friends/ colleagues who are meeting in the city:')
display(friend_slider)
Select the number of friends/ colleagues who are meeting in the city:
In [33]:
'''This cell can be omitted if you are '''
# Select random selection of locations
friend_size = 7

#S tations
home_stations = random.sample(station_list, friend_size)
home_stations
Out[33]:
['Jewell',
 'Flagstaff',
 'Newmarket',
 'Flemington Bridge',
 'Royal Park',
 'Southern Cross',
 'Parliament']

End of Optional cells

Create a button set of Train Stations within a given radius

In [34]:
print(f'Radius Size = {slider.value} km')
Radius Size = 5 km
In [35]:
# Define the list to store selected station
selected_station = home_stations.copy()

# selected_station = [] #Empty list to use if you are not using the friend size
In [36]:
# Define the function to add or remove station
def add_remove_station(station):
    global selected_station
    if station in selected_station:
        selected_station.remove(station)
    else:
        selected_station.append(station)
    update_button_color()

# Define a function to update the button color based on the selected station list
def update_button_color():
    for button in button_vbox.children:
        if button.description in selected_station:
            button.button_style = 'success'
        else:
            button.button_style = ''
In [37]:
# Create a VBox to display the buttons
button_vbox = widgets.VBox()

# Create the buttons and add them to the VBox
for station in station_list:
    button = widgets.Button(description=station)
    if station in selected_station:
        button.button_style = 'success'
    button.on_click(lambda event, station=station: add_remove_station(station))
    button_vbox.children += (button,)
In [38]:
print("Select Stations:")
# Display the VBox
display(button_vbox)
Select Stations:

Display the Selected Train Stations (Dataset)

In [39]:
selected_station_df = train_stations_CBD[train_stations_CBD['station'].isin(selected_station)]
selected_station_df[['station', 'lat', 'lon']]
Out[39]:
station lat lon
2 Parliament -37.811929 144.973408
6 Flagstaff -37.811827 144.956447
7 Flemington Bridge -37.788137 144.939322
8 Jewell -37.774850 144.958749
16 Newmarket -37.786905 144.928886
20 Royal Park -37.781183 144.951868
23 Southern Cross -37.818354 144.952193
In [40]:
# Isolate the lat, lon values for the starting stations
locations = [tuple([x['lat'], x['lon']]) for x in selected_station_df['geo_point_2d']]

# Display the location (lat/lon pairs)
locations
Out[40]:
[(-37.811928999999964, 144.97340800000006),
 (-37.811826999999994, 144.95644700000003),
 (-37.78813699999995, 144.93932200000006),
 (-37.77484999999996, 144.958749),
 (-37.78690499999999, 144.92888600000003),
 (-37.781183, 144.951868),
 (-37.818354, 144.95219300000008)]

Create a Funtion to fin the midpoint between all of the train stations

In [41]:
def find_central_midpoint(coords):
    '''Given a list of lat/lon pairs find the midpoint'''
    # Convert coordinates to radians
    latitudes, longitudes = zip(*coords)
    latitudes = [math.radians(lat) for lat in latitudes]
    longitudes = [math.radians(lon) for lon in longitudes]

    # Calculate the midpoint
    x = sum(math.cos(lat) * math.cos(lon) for lat, lon in zip(latitudes, longitudes))
    y = sum(math.cos(lat) * math.sin(lon) for lat, lon in zip(latitudes, longitudes))
    z = sum(math.sin(lat) for lat in latitudes)
    n = len(coords)
    lon_mid = math.atan2(y, x)
    lat_mid = math.atan2(z, math.sqrt(x**2 + y**2))
    return (math.degrees(lat_mid), math.degrees(lon_mid))
In [42]:
# find the geographical centre point between all stations
meeting_point = find_central_midpoint(locations)

# View the Geographocal Centre Points
meeting_point
Out[42]:
(-37.79617002057846, 144.95155215362925)
Visualise the Data

First we need to create a base-layer map that we can add all of our dataset to.

In [43]:
#Create the base layer map
m = folium.Map(
    # Lat, Lon for Melbourne CBD
    location=[-37.81368709240999, 144.95738102347036], 
    tiles="cartodbpositron",
    zoom_start=13, 
    control_scale=True,
    prefer_canvas=True, 
    width=800, 
    height=580
)

# Display the map
m
Out[43]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Create map layer clusters

A cluster is a collection of objects that we can add to a folium map as a single layer than can have thier visability turned on and off as a group.

In [44]:
# Create 4 food business clusters, 1 coworking space cluster and one for the central meeting zone

food_layer = MarkerCluster(name="Food").add_to(m)
drink_layer = MarkerCluster(name="Drink").add_to(m)
indoor_layer = MarkerCluster(name="Indoor").add_to(m)
outdoor_layer = MarkerCluster(name="Outdoor").add_to(m)

cowork_layer = MarkerCluster(name= "Coworking Spaces").add_to(m)

meeting_layer = MarkerCluster(name= "Central Meeting Location").add_to(m)

Icons

Identify some icons for each food/drink business group.

In [45]:
food_icon = 'glyphicon-cutlery'
drink_icon = 'glyphicon-glass'
indoor_icon = 'glyphicon-home'
outdoor_icon = 'glyphicon-tree-deciduous'

cowork_icon = 'glyphicon-lock'

Revisit the Data sets

Take a quick look at the two main datasets agian, Business seat counts, and Coworking Businesses.

In [46]:
# Food/ Drink business seats count Dataset
seats.head(2)
Out[46]:
seating_type trading_name business_type number_of_seats building_address business_address longitude latitude
0 Seats - Indoor +39 Pizzeria & Antipasteria Cafes and Restaurants 66 362-364 Little Bourke Street MELBOURNE VIC 3000 Ground 362 Little Bourke Street MELBOURNE VIC ... 144.96175 -37.81327
1 Seats - Indoor 1000 Wat Cafes and Restaurants 59 269-321 Lonsdale Street MELBOURNE VIC 3000 Shop LG11 287 Lonsdale Street MELBOURNE VIC 3000 144.96393 -37.81247
In [47]:
# Co-Working Businesses Dataset
cowork_details = coworking[['organisation', 'address', 'website', 'geopoint']].values.tolist()
cowork_details[1]
Out[47]:
['360 Collins',
 '360 Collins Street, Melbourne 3000',
 'https://officespace.com.au/melbourne/153369/',
 {'lon': 144.962311, 'lat': -37.816002}]
In [48]:
# Isolate the columns from the "seats" Dataset so that it can be used for all food/drink map clusters
columns = seats.columns.to_list()
Create the map layer details
In [49]:
# Add Food Layer
for row in food.itertuples():
    location = row[columns.index('latitude')+1], row[columns.index('longitude')+1]
    icon=folium.Icon(color='red', icon=food_icon, prefix='glyphicon')
    html = f"<h3>{row[columns.index('trading_name')+1]}</h3><br>Address: {row[columns.index('business_address')+1]} <br>Total Seats: {row[columns.index('number_of_seats')+1]}"
    iframe = folium.IFrame(html, width=300, height=130)
    popup = folium.Popup(iframe, max_width=300)
    marker = folium.Marker(location=location, popup=popup, icon=icon)
    food_layer.add_child(marker)

# Add Drink Layer
for row in drink.itertuples():
    location = row[columns.index('latitude')+1], row[columns.index('longitude')+1]
    icon=folium.Icon(color='blue', icon=drink_icon, prefix='glyphicon')
    html = f"{row[columns.index('trading_name')+1]}<br>Address: {row[columns.index('business_address')+1]} <br>Total Seats: {row[columns.index('number_of_seats')+1]}"
    iframe = folium.IFrame(html, width=300, height=130)
    popup = folium.Popup(iframe, max_width=300)
    marker = folium.Marker(location=location, popup=popup, icon=icon, color='blue')
    drink_layer.add_child(marker)

# Add Indoor Layer
for row in indoor.itertuples():
    location = row[columns.index('latitude')+1], row[columns.index('longitude')+1]
    icon=folium.Icon(color='purple', icon=indoor_icon, prefix='glyphicon')
    html = f"{row[columns.index('trading_name')+1]}<br>Address: {row[columns.index('business_address')+1]} <br>Total Seats: {row[columns.index('number_of_seats')+1]}"
    iframe = folium.IFrame(html, width=300, height=130)
    popup = folium.Popup(iframe, max_width=300)
    marker = folium.Marker(location=location, popup=popup, icon=icon)
    indoor_layer.add_child(marker)

# Add Outdoor Layer
for row in outdoor.itertuples():
    location = row[columns.index('latitude')+1], row[columns.index('longitude')+1]
    icon=folium.Icon(color='green', icon=outdoor_icon, prefix='glyphicon')
    html = f"{row[columns.index('trading_name')+1]}<br>Address: {row[columns.index('business_address')+1]} <br>Total Seats: {row[columns.index('number_of_seats')+1]}"
    iframe = folium.IFrame(html, width=300, height=130)
    popup = folium.Popup(iframe, max_width=300)
    marker = folium.Marker(location=location, popup=popup, icon=icon)
    outdoor_layer.add_child(marker)

 # Add Co Working Layer
for row in cowork_details:
    location = row[3]['lat'], row[3]['lon']
    
    icon=folium.Icon(color='lightgray', icon=cowork_icon, prefix='glyphicon')
    html = f'{row[0]}<br>Address: {row[1]} <br>Website: <a href="{row[2]}" target="blank">{row[2]}</a>'
    iframe = folium.IFrame(html, width=300, height=130)
    popup = folium.Popup(iframe, max_width=300)
    marker = folium.Marker(location=location, popup=popup, icon=icon)
    cowork_layer.add_child(marker)

# Add central location Layer   
meeting_place = folium.Circle(
    location=meeting_point,
    radius=1000, # meters
    color='red',
    fill=True,
    fill_opacity=0.2
)
meeting_layer.add_child(meeting_place)
    
    
folium.LayerControl().add_to(m)
Out[49]:
<folium.map.LayerControl at 0x27036745940>
View Interactive Map
In [50]:
m
Out[50]:
Make this Notebook Trusted to load map: File -> Trust Notebook
Conclusion

The above map provides an interactive resource for not only finidng the locatioon that is central to all parties, but also to find (and book using the business url) a coworking office space, and to also locate businesses nearby to that location which might be suitable to breakfast, lunch or after work drinks and nibbles.

This investigation can be expanded further to utilise actual addresses or tram/ bus stops (rather than train stations) as a means to find a central location between all parties that will be working together.